import { globalEmitter, IEvent } from "@feng3d/event";
import { watcher } from "@feng3d/watcher";
import { client } from "@feng3d/websocketclient";
import { PlayerInfo, update_player_info } from "@feng3d/websocketmessage";
import { Box3, LoadingManager, MathUtils, Matrix4, Mesh, MeshPhongMaterial, Object3D, Scene, Vector3 } from "three";
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
import { Capsule } from "three/examples/jsm/math/Capsule";
import URDFLoader, { URDFRobot } from "urdf-loader";
import { changeHighlightMaterial } from "./changeHighlightMaterial";
import { robots } from "./data";

/**
 * 玩家
 */
export class Player
{
    name: string = "Player";

    /**
     * 速度
     */
    velocity = new Vector3();

    /**
     * 方向
     */
    direction = new Vector3();

    /**
     * 胶囊碰撞器
     */
    collider = new Capsule(new Vector3(0, 0.35, 0), new Vector3(0, 1, 0), 0.35);
    clonesChanged: boolean;
    private _robotJointValues: { key: string, angle: number }[];

    get robot()
    {
        return this._robot;
    }
    set robot(value: URDFRobot)
    {
        if (this._robot?.parent) this._robot.parent.remove(this._robot);
        this._robot = value;
        if (this._robot)
        {
            this.object3d.add(this._robot);
            this.onload?.();
        }
    }
    private _robot: URDFRobot;

    object3d = new Object3D();
    onload: () => void;
    prePlayerInfo: PlayerInfo = {} as any;

    get robotName()
    {
        return this._robotName;
    }
    set robotName(value)
    {
        if (!value) return;
        this._robotName = value;
        const { scale, rootPath, path } = robots[value];
        Player.getRobot(((result) =>
        {
            if (this._robotName !== value) return;
            //
            const { robot, size } = result;
            this.robot = robot.clone(true);
            this.object3d.scale.set(scale, scale, scale);
            this.collider.radius = Math.max(size.x, size.z) / 2 * scale;
            this.collider.end.y = this.collider.start.y + Math.max(size.y * scale - this.collider.radius * 2, 0.01);
            
            requestAnimationFrame(()=>{

                //
                if (this._robotJointValues)
                {
                    this._robotJointValues.forEach((jointValue) =>
                    {
                        const joint = this.robot.joints[jointValue.key];
                        if (joint)
                        {
                            joint.setJointValue(jointValue.angle);
                        }
                    })
                    this._robotJointValues = undefined;
                    this.robot.updateMatrixWorld(true);
                }
            })
        }), rootPath, path);
        this.onNeedUpdate();
    }
    private _robotName: string;

    constructor(private scene: Scene, robotPath: string = "iiwa14_glb")
    {
        this.robotName = robotPath;

        scene.add(this.object3d);

        watcher.watchobject(this as Player, {
            collider: {
                start: { x: 0, y: 0, z: 0 },
                end: { x: 0, y: 0, z: 0 },
            },
            velocity: { x: 0, y: 0, z: 0 },
        }, this.onNeedUpdate, this);

        globalEmitter.on("update_player_info", this.onupdate_player_info, this);
    }

    savePose()
    {
        if (this._robot)
        {
            const joints = Object.keys(this._robot.joints).map((key) =>
            {
                const joint = this._robot.joints[key];
                if (joint.jointType === "fixed") return;
                return { name: key, value: joint.angle as number };
            }).filter((x) => !!x);

            const cloneInfo = { robotName: this._robotName, matrixWorld: this._robot.matrixWorld.toArray(), joints };

            this.addClone(cloneInfo);

            this.clonesChanged = true;
            this.onNeedUpdate();
        }
    }

    addClone(cloneInfo: { robotName: string; matrixWorld: number[]; joints: { name: string; value: number; }[]; })
    {
        const { path, rootPath, scale } = robots[cloneInfo.robotName];

        this.clones.push({ ...cloneInfo, clone: undefined });
        const newClone = this.clones[this.clones.length - 1];

        Player.getRobot((result) =>
        {
            if (!this.clones.includes(newClone)) return;

            const clone = result.robot.clone(true);

            cloneInfo.joints.forEach((jointInfo) =>
            {
                const joint = clone.joints[jointInfo.name];
                if (joint)
                {
                    joint.setJointValue(jointInfo.value);
                }
            });

            const matrixWorld = new Matrix4().fromArray(cloneInfo.matrixWorld)

            matrixWorld.decompose(clone.position, clone.quaternion, clone.scale);

            changeHighlightMaterial(clone, false, true, alphaMaterial);

            this.scene.add(clone);

            newClone.clone = clone;

        }, rootPath, path)
    }

    clearSavedPoses()
    {
        for (const clone of this.clones)
        {
            this.scene.remove(clone.clone);
        }
        this.clones = [];
        this.clonesChanged = true;
        this.onNeedUpdate();
    }

    private clones: {
        clone: URDFRobot
        robotName: string
        matrixWorld: number[]
        joints: {
            name: string;
            value: number;
        }[]
    }[] = [];

    private onupdate_player_info(event: IEvent<update_player_info>)
    {
        const data = event.data;
        if (data.data.name === client.username) return;
        if (data.data.name !== this.name) return;

        this.onUpdateData(data.data);
    }

    private isrequestUpdate = false;
    private onNeedUpdate()
    {
        if (this.isrequestUpdate) return;
        this.isrequestUpdate = true;
        requestAnimationFrame(() =>
        {
            this.onUpdate();
            this.isrequestUpdate = false;
        });
    }

    jointInfosChanged()
    {
        this.onNeedUpdate();
    }

    /**
     * 更新玩家位置
     */
    onUpdate()
    {
        this.object3d.position.x = this.collider.start.x;
        this.object3d.position.y = this.collider.start.y - this.collider.radius;
        this.object3d.position.z = this.collider.start.z;

        if (this.name === client.username)
        {
            const { x, z } = this.velocity;
            if (Math.abs(x) > 0.01 || Math.abs(z) > 0.01)
            {
                this.updateRotation(x, z);
            }

            let needSend = !this.prePlayerInfo;
            const playerInfo: PlayerInfo = {
                name: this.name,
            };
            if (!this.prePlayerInfo?.position
                || Math.abs(this.prePlayerInfo.position.x - this.collider.start.x) > 0.01
                || Math.abs(this.prePlayerInfo.position.y - this.collider.start.y) > 0.01
                || Math.abs(this.prePlayerInfo.position.z - this.collider.start.z) > 0.01
            )
            {
                needSend = true;
                playerInfo.position = { x: this.collider.start.x, y: this.collider.start.y, z: this.collider.start.z };
                this.prePlayerInfo.position = playerInfo.position;
            }
            if (!this.prePlayerInfo?.rotation
                || Math.abs(this.prePlayerInfo.rotation.x - this.object3d.rotation.x) > 0.01
                || Math.abs(this.prePlayerInfo.rotation.y - this.object3d.rotation.y) > 0.01
                || Math.abs(this.prePlayerInfo.rotation.z - this.object3d.rotation.z) > 0.01
            )
            {
                needSend = true;
                playerInfo.rotation = { x: this.object3d.rotation.x, y: this.object3d.rotation.y, z: this.object3d.rotation.z };
                this.prePlayerInfo.rotation = playerInfo.rotation;
            }

            if (this.robot)
            {
                this.prePlayerInfo.jointValues = this.prePlayerInfo.jointValues || {};
                const jointValues: { [key: string]: number; } = {};
                for (const key in this.robot.joints)
                {
                    const angle = this.robot.joints[key].angle as number;

                    if (angle !== this.prePlayerInfo.jointValues?.[key])
                    {
                        needSend = true;
                        jointValues[key] = angle;
                        this.prePlayerInfo.jointValues[key] = angle;
                        playerInfo.jointValues = jointValues;
                    }
                }
            }

            if (this.prePlayerInfo.robotName !== this.robotName)
            {
                needSend = true;
                playerInfo.robotName = this.robotName;
                this.prePlayerInfo.robotName = this.robotName;
            }

            if (this.clonesChanged)
            {
                this.clonesChanged = false;
                const clones = this.clones.map((clone) =>
                {
                    return {
                        robotName: clone.robotName,
                        matrixWorld: clone.matrixWorld,
                        joints: clone.joints,
                    };
                });
                playerInfo.clones = clones;
                needSend = true;
            }

            if (needSend)
            {
                // 更新玩家位置信息
                globalEmitter.emit("send_to_server", {
                    type: "update_player_info", data: playerInfo,
                });
            }
        }
    }

    /**
     * 通过速度更新旋转
     */
    private updateRotation(x: number, z: number)
    {
        const targetRotation = this.object3d.rotation.clone();
        const rotationMatrix = new Matrix4().lookAt(new Vector3(), new Vector3(x, 0, z), new Vector3(0, 1, 0));
        targetRotation.setFromRotationMatrix(rotationMatrix);

        this.object3d.rotation.x = targetRotation.x;
        this.object3d.rotation.y = targetRotation.y;
        this.object3d.rotation.z = targetRotation.z;
    }

    onUpdateData(data: PlayerInfo)
    {
        this.name = data.name;

        if (data.position)
        {
            this.collider.start.x = data.position.x;
            this.collider.start.y = data.position.y;
            this.collider.start.z = data.position.z;
        }
        if (data.rotation)
        {
            this.object3d.rotation.x = data.rotation.x;
            this.object3d.rotation.y = data.rotation.y;
            this.object3d.rotation.z = data.rotation.z;
        }
        if (data.jointValues)
        {
            for (const key in data.jointValues)
            {
                const angle = data.jointValues[key];
                if (this.robot)
                {
                    if (this.robot.joints[key])
                    {
                        this.robot.joints[key].setJointValue(angle);
                    }
                }
                else
                {
                    this._robotJointValues = this._robotJointValues || [];
                    this._robotJointValues.push({ key, angle });
                }
            }
        }
        if (data.robotName)
        {
            this.robotName = data.robotName;
        }
        if (data.clones)
        {
            if (data.clones.length === 0)
            {
                this.clearSavedPoses();
            }
            for (let i = this.clones.length; i < data.clones.length; i++)
            {
                this.addClone(data.clones[i]);
            }
        }
    }

    private static robotMap = new Map<string, { robot: URDFRobot, size: Vector3 }>();
    static getRobot(callback: (result: { robot: URDFRobot, size: Vector3 }) => void, root: string = "", path = "../../resources/models/arms/iiwa14/iiwa14_glb")
    {
        const result = this.robotMap.get(root + path);
        if (result)
        {
            callback(result);
            return;
        }

        let robot: URDFRobot;

        // const arms = "iiwa7";
        // const arms = "iiwa14";
        // const arms = "iiwa14/iiwa14";
        // const arms = "rm75/rm75";
        // const arms = "ur5e/ur5e";
        // const arms = "ur10e/ur10e";
        // const arms = "xarm7/xarm7";

        // Load robot
        const manager = new LoadingManager();
        manager.resolveURL = url =>
        {
            return root + url;
        };
        const loader = new URDFLoader(manager);

        loader.loadMeshCb = function (path, manager, done)
        {


            const ext = path.split(/\./g).pop().toLowerCase();
            switch (ext)
            {

                case 'gltf':
                case 'glb':
                    new GLTFLoader(manager).load(
                        path,
                        result => done(result.scene),
                        null,
                        err => done(null, err as any),
                    );
                    break;
                case 'obj':
                    new OBJLoader(manager).load(
                        path,
                        result => done(result),
                        null,
                        err => done(null, err as any),
                    );
                    break;
                case 'dae':
                    new ColladaLoader(manager).load(
                        path,
                        result => done(result.scene),
                        null,
                        err => done(null, err as any),
                    );
                    break;
                case 'stl':
                    new STLLoader(manager).load(
                        path,
                        result =>
                        {
                            const material = new MeshPhongMaterial();
                            const mesh = new Mesh(result, material);
                            done(mesh);
                        },
                        null,
                        err => done(null, err as any),
                    );
                    break;
            }
        };

        loader.load(path, result =>
        {
            robot = result;
        });

        // wait until all the geometry has loaded to add the model to the scene
        manager.onLoad = () =>
        {

            robot.rotation.x = Math.PI / 2;
            robot.traverse(c =>
            {
                c.castShadow = true;
            });
            for (let i = 1; i <= 16; i++)
            {
                if (!robot.joints[`HP${i}`]) continue;

                robot.joints[`HP${i}`].setJointValue(MathUtils.degToRad(30));
                robot.joints[`KP${i}`].setJointValue(MathUtils.degToRad(120));
                robot.joints[`AP${i}`].setJointValue(MathUtils.degToRad(-60));

            }
            robot.updateMatrixWorld(true);

            const bb = new Box3();
            bb.setFromObject(robot);

            robot.position.y -= bb.min.y;
            const size = bb.getSize(new Vector3());

            callback({ robot, size })

            this.robotMap.set(root + path, { robot, size });
        };
    }

    remove()
    {
        watcher.unwatchobject(this as Player, {
            collider: {
                start: { x: 0, y: 0, z: 0 },
                end: { x: 0, y: 0, z: 0 },
            }
        }, this.onUpdate, this);

        globalEmitter.off("update_player_info", this.onupdate_player_info, this);

        this.object3d.removeFromParent();
        this.object3d = null;

        this.clones.forEach((v)=>{
            v.clone?.removeFromParent();
        })
    }
}


// The highlight material
const alphaMaterial =
    new MeshPhongMaterial({
        shininess: 10,
        color: 0xffffff,
        emissive: 0xffffff,
        emissiveIntensity: 0.25,
        transparent: true,
        opacity: 0.5,
    });